using System;
using System.Collections;
using System.Text.RegularExpressions;
//using System.Diagnostics;
using System.Xml;

namespace Antix
{
    /// <summary>
    /// <para>DateTimeExt is able to parse RFC2822/RFC822 formatted dates.</para>
    /// </summary>
    public class DateTimeFunctions
    {
        #region Nowish

        /// <summary>
        /// Less accurate version of Now, only accurate to the minute
        /// </summary>
        public static DateTime Nowish
        {
            get
            {
                return new DateTime(
                    DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day,
                    DateTime.Now.Hour, DateTime.Now.Minute, 0);
            }
        }

        #endregion

        #region DaySplit

        /// <summary>
        /// <para>Split the date range into number of days before and after a number of days</para>
        /// </summary>
        /// <param name="dateFrom">Date from</param>
        /// <param name="dateTo">Date to</param>
        /// <param name="splitAt">Number of days to split at</param>
        /// <returns>Days before and days after the split (2 int element array)</returns>
        public static int[] DaySplit(DateTime dateFrom, DateTime dateTo, int splitAt)
        {
            int days = dateTo.Subtract(dateFrom).Days;
            if (dateFrom.AddDays(splitAt) > dateTo)
            {
                return new int[] { days, 0 };
            }
            else
            {
                return new int[] { splitAt, days - splitAt };
            }
        }

        #endregion

        #region DaySplitMonths

        /// <summary>
        /// <para>Split the date range into number of days before and after a number of days per month</para>
        /// </summary>
        /// <param name="dateFrom">Date from</param>
        /// <param name="dateTo">Date to</param>
        /// <param name="splitAt">Number of days to split at</param>
        /// <returns>Array</returns>
        public static System.Array DaySplitMonths(DateTime dateFrom, DateTime dateTo, int splitAt)
        {
            ArrayList months = new ArrayList();
            int daysTotal = dateTo.Subtract(dateFrom).Days;
            int daysSofar = 0;

            while (dateFrom <= dateTo)
            {
                DateTime lastDayOfMonth = LastDayOfMonth(dateFrom);
                int daysInMonth = lastDayOfMonth.Subtract(dateFrom).Days;

                // not less than the total days
                if (daysInMonth > daysTotal) { daysInMonth = daysTotal; }

                // add new month split
                if (daysSofar + daysInMonth < splitAt)
                {
                    // all before split
                    months.Add(new int[] { daysInMonth, 0 });
                }
                else if (daysSofar + daysInMonth <= daysTotal)
                {
                    // days before and after split
                    months.Add(new int[] { splitAt - daysSofar, daysSofar + daysInMonth - splitAt });
                }
                else
                {
                    // days after the split
                    months.Add(new int[] { 0, daysTotal - daysSofar });
                }

                daysSofar += daysInMonth;
                dateFrom = lastDayOfMonth.AddDays(1);
            }

            return months.ToArray();
        }

        #endregion

        #region First/Last Day Of Month

        /// <summary>
        /// <para>Get the first day of the month given a date</para>
        /// </summary>
        /// <param name="date">Date</param>
        /// <returns>First day of the same month</returns>
        public static DateTime FirstDayOfMonth(DateTime date)
        {
            return new DateTime(date.Year, date.Month, 1);
        }

        /// <summary>
        /// <para>Get the last day of the month given a date</para>
        /// </summary>
        /// <param name="date">Date</param>
        /// <returns>Last day of the same month</returns>
        public static DateTime LastDayOfMonth(DateTime date)
        {
            return FirstDayOfMonth(date).AddMonths(1).AddDays(-1);
        }

        #endregion

        #region Quarters

        /// <summary>
        /// <para>Get the start of the quarter given a date</para>
        /// </summary>
        /// <param name="date">Date</param>
        /// <returns>Start day of the quarter</returns>
        public static DateTime StartOfQuarter(DateTime date)
        {
            return new DateTime(date.Year, ((int)Math.Floor((decimal)date.Month / 3) * 3) + 1, 1);
        }

        /// <summary>
        /// <para>Return the quarter given a date</para>
        /// </summary>
        /// <param name="date"></param>
        /// <returns></returns>
        public static int Quarter(DateTime date)
        {
            return (int)Math.Floor(((decimal)date.Month - 1) / 3) + 1;
        }

        #endregion

        #region Parsing

        /// <summary>
        /// Parse, if not return null or minimum date value
        /// </summary>
        public static DateTime? Parse(string dateTimeString, bool allowNull)
        {
            DateTime dateTime;
            if (TryParse(dateTimeString, out dateTime))
            {
                return dateTime;
            }

            return allowNull ? new DateTime?() : DateTime.MinValue;
        }

        /// <summary>
        /// Parse, allow null to be returned
        /// </summary>
        public static DateTime? Parse(string dateTimeString)
        {
            return Parse(dateTimeString, true);
        }

        /// <summary>
        /// Parse, if not return a default
        /// </summary>
        public static DateTime Parse(string dateTimeString, DateTime defaultValue)
        {
            DateTime dateTime;
            if (TryParse(dateTimeString, out dateTime))
            {
                return dateTime;
            }

            return defaultValue;
        }

        #region TryParse

        /// <summary>
        /// Attempt to parse a string into a date
        /// </summary>
        /// <param name="dateTimeString">String to parse</param>
        /// <param name="dateTime">Parsed datetime</param>
        /// <returns>Parse was successful</returns>
        public static bool TryParse(string dateTimeString, out DateTime dateTime)
        {
            if (dateTimeString != null && dateTimeString.Trim().Length > 0)
            {

                Match m = rfc2822.Match(dateTimeString);
                if (m.Success)
                {
                    try
                    {
                        int dd = Int32.Parse(m.Groups[1].Value);
                        int mth = MonthFromName(m.Groups[2].Value);
                        int yy = Int32.Parse(m.Groups[3].Value);
                        // following year completion is compliant with RFC 2822.
                        yy = (yy < 50 ? 2000 + yy : (yy < 1000 ? 1900 + yy : yy));

                        // check for time
                        if (m.Groups[4].Value == "")
                        {
                            dateTime = new DateTime(yy, mth, dd);
                        }
                        else
                        {
                            int hh = Int32.Parse(m.Groups[4].Value);
                            int mm = Int32.Parse(m.Groups[5].Value);

                            // check for seconds
                            if (m.Groups[6].Value == "")
                            {
                                dateTime = new DateTime(yy, mth, dd, hh, mm, 0);
                            }
                            else
                            {
                                int ss = Int32.Parse(m.Groups[6].Value);
                                dateTime = new DateTime(yy, mth, dd, hh, mm, ss);
                            }

                            // check for zone
                            if (m.Groups[7].Value != "")
                            {
                                string zone = m.Groups[7].Value;
                                dateTime = dateTime.AddHours(RFCTimeZoneToGMTBias(zone) * -1);
                            }
                        }

                        return true;
                    }
                    catch (Exception e)
                    {
                        throw new FormatException("DateTime parse failure", e);
                    }
                }
            }

            dateTime = DateTime.MinValue;
            return false;
        }

        private static Regex rfc2822
            = new Regex(
                @"\s*(?:(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s*,\s*)?"
                + @"(\d{1,2})[/\-|\s+]"
                + @"(\d{1,2}|Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec|January|Feburary|March|April|May|June|July|August|September|October|November|December)[/\-|\s+]"
                + @"(\d{2,})(?:\s+(\d{1,2})\s*:\s*(\d{2})\s*(?::\s*(\d{2}))?\s*([+\-]\d{4}|UT|GMT|EST|EDT|CST|CDT|MST|MDT|PST|PDT|[A-IK-Z])?)?",
                RegexOptions.Compiled | RegexOptions.IgnoreCase);

        #endregion

        #region Time Zone

        private struct TZB
        {
            public TZB(string z, int b) { Zone = z; Bias = b; }
            public string Zone;
            public int Bias;
        }

        private const int timeZones = 35;
        private static TZB[] ZoneBias = new TZB[timeZones]
				{
					new TZB("GMT", 0),     new TZB("UT", 0),
					new TZB("EST", -5*60), new TZB("EDT", -4*60),
					new TZB("CST", -6*60), new TZB("CDT", -5*60),
					new TZB("MST", -7*60), new TZB("MDT", -6*60),
					new TZB("PST", -8*60), new TZB("PDT", -7*60),
					new TZB("Z", 0),       new TZB("A", -1*60),
					new TZB("B", -2*60),   new TZB("C", -3*60),
					new TZB("D", -4*60),   new TZB("E", -5*60),
					new TZB("F", -6*60),   new TZB("G", -7*60),
					new TZB("H", -8*60),   new TZB("I", -9*60),
					new TZB("K", -10*60),  new TZB("L", -11*60),
					new TZB("M", -12*60),  new TZB("N", 1*60),
					new TZB("O", 2*60),    new TZB("P", 3*60),
					new TZB("Q", 4*60),    new TZB("R", 3*60),
					new TZB("S", 6*60),    new TZB("T", 3*60),
					new TZB("U", 8*60),    new TZB("V", 3*60),
					new TZB("W", 10*60),   new TZB("X", 3*60),
					new TZB("Y", 12*60)
				};

        private static double RFCTimeZoneToGMTBias(string zone)
        {

            string s;

            if (zone.IndexOfAny(new char[] { '+', '-' }) == 0)  // +hhmm format
            {
                int fact = (zone.Substring(0, 1) == "-" ? -1 : 1);
                s = zone.Substring(1).TrimEnd();
                double hh = Math.Min(23, Int32.Parse(s.Substring(0, 2)));
                double mm = Math.Min(59, Int32.Parse(s.Substring(2, 2))) / 60;
                return fact * (hh + mm);
            }
            else
            { // named format
                s = zone.ToUpper().Trim();
                for (int i = 0; i < timeZones; i++)
                    if (ZoneBias[i].Zone.Equals(s))
                    {
                        return ZoneBias[i].Bias / 60;
                    }
            }
            return 0.0;
        }

        private static DateTime AddOffset(string offsetOp, string datetime, int tzoneIndex)
        {

            string[] offset = datetime.Substring(tzoneIndex + 1).Split(new char[] { ':' });
            string original = datetime;
            datetime = datetime.Substring(0, tzoneIndex);

            if (datetime.IndexOf(":") == datetime.LastIndexOf(":"))
            { //check if seconds part is missing
                datetime = datetime + ":00";
            }

            DateTime toReturn = XmlConvert.ToDateTime(datetime, XmlDateTimeSerializationMode.Local);

            try
            {

                switch (offsetOp)
                {

                    case "+":
                        toReturn = toReturn.Subtract(new TimeSpan(Int32.Parse(offset[0]), Int32.Parse(offset[1]), 0));
                        break;

                    case "-":
                        toReturn = toReturn.Add(new TimeSpan(Int32.Parse(offset[0]), Int32.Parse(offset[1]), 0));
                        break;

                }

                return toReturn; //.ToLocalTime(); //we treat all dates in feeds as if they are local time (later)

            }
            catch (IndexOutOfRangeException)
            {

                throw new FormatException("RES_ExceptionRFC2822InvalidTimezoneFormatMessage");
            }

        }

        #endregion

        #endregion

        #region MonthFromName

        /// <summary>
        /// Return the number of the month from the name
        /// </summary>
        /// <param name="name">Name of the month</param>
        /// <returns>Month number</returns>
        public static int MonthFromName(string name)
        {
            switch (name.ToLower().TrimStart('0'))
            {
                case "1":
                case "jan":
                case "january": return 1;
                case "2":
                case "feb":
                case "feburary": return 2;
                case "3":
                case "mar":
                case "march": return 3;
                case "4":
                case "apr":
                case "april": return 4;
                case "5":
                case "may": return 5;
                case "6":
                case "jun":
                case "june": return 6;
                case "7":
                case "jul":
                case "july": return 7;
                case "8":
                case "aug":
                case "august": return 8;
                case "9":
                case "sep":
                case "sept":
                case "september": return 9;
                case "10":
                case "oct":
                case "october": return 10;
                case "11":
                case "nov":
                case "november": return 11;
                case "12":
                case "dec":
                case "december": return 12;
                default: return 0;
            }
        }

        #endregion

        #region Range Formatting

        /// <summary>
        /// Format a datetime range
        /// </summary>
        public static string FormatDateTimeRange(DateTime from, DateTime? to, string format)
        {
            if (to == null || from == to)
            {
                // same time same day
                return from.ToString(format);
            }
            else if (from.Date == to.Value.Date)
            {
                // same day
                Regex rangeRegex = new Regex(@"[\s\/\-,\.Hh:mSst]+");
                string rangeFormat = rangeRegex.Match(format).Value;

                return from.ToString(format.Replace(rangeFormat, "!"))
                    .Replace("!", from.ToString(rangeFormat) + " to " + to.Value.ToString(rangeFormat));
            }
            else if (from.Month == to.Value.Month && from.Year == to.Value.Year)
            {
                // same month and year
                Regex rangeRegex = new Regex(@"[\s\/\-,\.DdHh:mSst]+");
                string rangeFormat = rangeRegex.Match(format).Value;
                string range;
                switch (rangeFormat)
                {
                    case "d":
                        range = from.Day + " to " + to.Value.Day; break;
                    case "dd":
                        range = from.Day.ToString("00") + " to " + to.Value.Day.ToString("00"); break;
                    default:
                        range = from.ToString(rangeFormat) + " to " + to.Value.ToString(rangeFormat); break;
                }
                return from.ToString(format.Replace(rangeFormat, "!")).Replace("!", range);
            }
            else if (from.Year == to.Value.Year)
            {
                // same year
                Regex rangeRegex = new Regex(@"[\s\/\-,\.dDMHh:mSst]+");
                string rangeFormat = rangeRegex.Match(format).Value;

                return from.ToString(format.Replace(rangeFormat, "!"))
                    .Replace("!", from.ToString(rangeFormat) + " to " + to.Value.ToString(rangeFormat));
            }
            else
            {
                return from.ToString(format) + " to " + to.Value.ToString(format);
            }
        }

        /// <summary>
        /// Format a date range
        /// </summary>
        public static string FormatDateRange(DateTime from, DateTime? to, string format)
        {
            // remove the time formatting
            return FormatDateTimeRange(
                from, to,
                new Regex(@"\s*[hH]{1,2}:[m]{1,2}([s]{1,2})?\s*t*\s*").Replace(format, ""));
        }

        /// <summary>
        /// Format a time range
        /// </summary>
        public static string FormatTimeRange(DateTime from, DateTime? to, string format)
        {
            // remove the date formatting
            format = new Regex(@"\s*(d{1,4}|M{1,4}|y{2,4})\s*").Replace(format, "");
            if (to.HasValue)
            {
                return from.ToString(format) + " to " + to.Value.ToString(format);
            }
            else
            {
                return from.ToString(format);
            }
        }

        #endregion
    }
}